Skip to content

fix(intro): débloque l'intro des derniers faits (7×9/8×9/9×9) - #67

Merged
isc merged 2 commits into
mainfrom
fix/intro-derniers-faits
Jun 15, 2026
Merged

fix(intro): débloque l'intro des derniers faits (7×9/8×9/9×9)#67
isc merged 2 commits into
mainfrom
fix/intro-derniers-faits

Conversation

@isc

@isc isc commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Contexte

Feedback utilisateur (Gwennaelle) : « le même bug qu'avant sur l'image, il y a des cases non visibles ». Diagnostic : sur son profil (33/36 introduits), les 3 faits 7×9, 8×9, 9×9 n'étaient jamais introduits → coin bas-droite de l'image mystère définitivement vide.

Simulation sur son profil réel (vrai composeSession + processAnswer) :

Profil de réponse avant après
sans-faute (adulte) introduits ~S6 S1–S3
rate ~1 fois sur 3 jamais S1–S3
rate la table de 9 jamais S1–S3

Cause racine — deux verrous distincts

  1. recentlyIntroduced mal déterminé (sessionComposer). Le filtre anti-interférence 48h s'appuyait sur history[0].date. Pour un fait dominé au placement, cette date n'est pas son intro mais sa 1ʳᵉ révision : dès que la table de 9 était revue, ses faits passaient pour « introduits récemment » et bloquaient l'intro des faits partageant l'opérande 9. → champ explicite introducedAt, posé uniquement à l'écran d'intro réel. Les faits dominés ne l'ont jamais → exclus de la fenêtre 48h. Conforme à specs §1.2 (qui décrivait déjà ce comportement).

  2. TAIL_INTRO_THRESHOLD trop bas (leitner). L'exception « fin de parcours » (introduire même avec des faits en boîte 1) ne se déclenchait qu'à ≤2 restants — l'auteur supposait que seuls 8×9/9×9 pouvaient rester bloqués. Le trou de dominance du placement couvre tout le coin difficile (3+ faits). 2 → 7 (≥29/36 introduits).

Les deux sont nécessaires : (1) débloque le filtre (cas adulte/sans-faute), (2) débloque la gate (cas enfant en difficulté).

Décisions de design

  • Anti-famine écarté. Une 1ʳᵉ piste (forcer l'intro quand le filtre vide tout) marchait mais violait l'espacement 48h. Corriger recentlyIntroduced à la racine respecte l'espacement (9×9 attend bien 48h après le lot 7×9/8×9).
  • Aucune migration. Profils existants sans introducedAt → « non introduits récemment » → débloque immédiatement tout profil coincé. Backfill rejeté (ré-introduirait la confusion).
  • introducedAt uniquement côté multiplication. La division n'a pas de fenêtre 48h (§11.6) — son anti-interférence passe par questionConflict (même dividende) — donc le champ n'y a pas de raison d'être. La gate shouldIntroduceNew reste mutualisée, mais vérifiée inerte pour la division (intro garrotée par la maîtrise du parent multiplicatif ; identique au baseline sur 4 scénarios simulés).

Tests

  • Régression dédiée : queue introduite en quelques séances même en ratant systématiquement les ×9.
  • Gate : relâche à ≤7 restants, maintient la protection si beaucoup restent.
  • Garde-fou 48h repiloté par introducedAt.
  • Specs §3.4bis + changelog in-app mis à jour.

🤖 Generated with Claude Code

Deux verrous empêchaient l'introduction des faits les plus durs — ceux
que le seeding par dominance du placement ne peut pas inférer (au-delà
du dernier fait réussi : typiquement 7×9, 8×9, 9×9). Résultat : l'image
mystère restait incomplète, parfois à vie pour un enfant qui rate la
table de 9 (feedback utilisateur).

1. recentlyIntroduced (filtre 48h, sessionComposer) s'appuyait sur
   history[0].date, qui pour un fait DOMINÉ au placement n'est pas sa
   date d'intro mais sa 1ʳᵉ révision. Dès que la table de 9 était revue,
   ses faits passaient pour « introduits récemment » et bloquaient
   l'intro des faits similaires. On introduit un champ explicite
   `introducedAt`, posé uniquement à l'écran d'intro réel (App.tsx) ;
   les faits dominés ne l'ont jamais → ils n'entrent plus dans la
   fenêtre 48h. Met le code en conformité avec specs §1.2.

2. TAIL_INTRO_THRESHOLD passait l'exception « fin de parcours » à ≤2
   restants, en supposant que seuls 8×9/9×9 pouvaient rester bloqués.
   Le trou de dominance couvre tout le coin difficile (3+ faits). Seuil
   2 → 7 (≥29/36 introduits = fin de parcours).

Migration : aucune. Les profils existants n'ont pas `introducedAt` →
traités comme « non introduits récemment », ce qui débloque
immédiatement tout profil coincé (comportement voulu).

Tests : régression dédiée (queue introduite même en ratant les ×9),
gate ≤7 restants, garde-fou 48h piloté par introducedAt. Specs §3.4bis
+ changelog mis à jour.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Preview supprimée (PR fermée). Les URLs ne sont plus accessibles.

Le champ était écrit mais jamais lu : composeDivisionSession n'a pas de
fenêtre 48h (§11.6), et l'anti-interférence division passe par
questionConflict (même dividende). On le garde strictement là où il est
lu — la multiplication.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@isc
isc merged commit 41176f3 into main Jun 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant